'''
Generate a maneuvering board.
 
Numerous PDFs at:
http://www.nga.mil/portal/site/maritime/?epi_menuItemID=35ad5b8aabcefa1a0fc133443927a759&epi_menuID=e106a3b5e50edce1fec24fd73927a759&epi_baseMenuID=e106a3b5e50edce1fec24fd73927a759

There's a 10 MB "Maneuvering Board Manual" at
http://www.nga.mil/portal/site/maritime/?epi_menuItemID=22707e6ae3d0c1b25b2a7fbd3227a759&epi_menuID=35ad5b8aabcefa1a0fc133443927a759&epi_baseMenuID=e106a3b5e50edce1fec24fd73927a759
(It's Chapter 6 of the "Radar and Maneuvering Board Manual" book,
publication 1310, National Imagery and Mapping Agency).  The site is under
the name of NGA; NGA is National Geospatial Intelligence Agency.
 
'''
from __future__ import division
import pdb, sys, string
from math import sin, cos, log10, pi
from g import *
from versiondec import VersionDec as vd
 
debug = 1
use_color = 1

draw_radials   = 1
draw_scales    = 1
draw_degrees   = 1
draw_box       = 1
draw_nomograph = 1

if debug:
    draw_radials   = 0
    draw_scales    = 1
    draw_degrees   = 0
    draw_box       = 1
    draw_nomograph = 1

Xcenter = 4
Ycenter = 4.75
Diameter = 6

center_cross_size = 0.01  # Fraction of box size
tick_length = 0.05
tick_line_width = 0.01
tick_color = black
ten_degree_tick_color = black
number_height = 0.015  # Fraction of box size
number_background = white
bearing_color = black
reciprocal_bearing_color = black
radial_color = grey(0.75)
base_line_color = grey(0.0)

def SetUpGlobals():
    def Color():
        global base_line_color
        base_line_color = gray(0.0)
        global ten_degree_tick_color
        ten_degree_tick_color = (1, .5, .5)
        global reciprocal_bearing_color
        reciprocal_bearing_color = navyblue
        global bearing_color
        bearing_color = red
        global radial_color
        radial_color = (0.8, 0.8, 1)
    if use_color:
        Color()
SetUpGlobals()

def DrawDegreeScale(diameter):
    def DrawScale(diameter):
        push()
        lineWidth(tick_line_width)
        lineColor(base_line_color)
        move(-diameter/2, -diameter/2)
        if draw_box:
            rectangle(diameter, diameter)
        text_height = number_height*diameter
        textSize(text_height)
        radius = diameter/2
        # Draw an enveloping circle
        move(0, 0)
        if not draw_degrees:
            pop()
            return
        circle(diameter)
        # Draw tick marks every degree
        push()
        for angle in range(360):
            line(radius, 0, radius-tick_length, 0)
            rotate(1)
        pop()
        # Draw tick marks every 5 degrees
        push()
        for angle in range(int(360/5)):
            line(radius, 0, radius-2*tick_length, 0)
            rotate(5)
        pop() 
        rotate(90)  # Put 0 degrees at top of page
        # Draw tick marks every 10 degrees and add a text label
        textColor(bearing_color)
        for angle in range(0, 360, 10):
            x = radius - 2*tick_length
            lineColor(ten_degree_tick_color)
            lineWidth(2*tick_line_width)
            line(radius, 0, x, 0)
            lineWidth(tick_line_width)
            lineColor(tick_color)
            push()
            if angle >= 90 and angle <= 270:
                translate(radius + text_height, 0)
            else:
                translate(radius + text_height/3, 0)
            move(0, 0)
            rotate(-90)
            if angle >= 90 and angle <= 270:
                rotate(180)
            # Draw a filled background to wipe out any existing stuff
            push()
            fillColor(number_background)
            fillOn()
            lineOff()
            move(-text_height, -text_height/7.5)
            rectangle(2*text_height, text_height)
            pop()
            ctext("%d" % angle)
            pop()
            # Draw reciprocal bearings
            push()
            textColor(reciprocal_bearing_color)
            textSize(3/4 * text_height)
            if angle >= 90 and angle <= 270:
                translate(radius - (tick_length + 0.7*text_height), 0)
            else:
                translate(radius - (tick_length + 1.2*text_height), 0)
            move(0, 0)
            rotate(-90)
            if angle >= 90 and angle <= 270:
                rotate(180)
            deg = (angle + 180) % 360
            ctext("%d" % deg)
            pop()
            rotate(-10)  # Increasing angle clockwise
        pop() 
    def DrawRadials(diameter):
        push()
        r = diameter/2
        clipRectangle(-r, -r, r, r)
        lineColor(radial_color)
        # Draw radials
        push()
        for angle in range(0, 360, 10):
            line(diameter/20, 0, 2*diameter, 0)
            rotate(10)
        pop()
        # Draw the centered circles
        move(0, 0)
        for ix in range(1, 15):
            circle(diameter * ix/10)
        # Draw the center cross
        lineColor(base_line_color)
        s = center_cross_size * diameter
        line(0, 0, s, 0)
        line(0, 0, 0, s)
        line(0, 0, -s, 0)
        line(0, 0, 0, -s)
        # Label the concentric circles
        textColor(reciprocal_bearing_color)
        text_height = number_height*diameter * 3/4
        textSize(text_height)
        push()
        for angle in (0, 90, 180, 270):
            for ix in range(1, 10):
                r = ix/10 * diameter/2
                x = r * cos(angle*pi/180)
                y = r * sin(angle*pi/180)
                if angle == 180:
                    x -= text_height*3/4
                move(x, y)
                text("%d" % ix)
        pop()
        pop()
    push()
    translate(Xcenter, Ycenter)
    if draw_radials:
        DrawRadials(diameter)
    DrawScale(diameter)
    pop()

def DrawScales(diameter):
    push()
    X = Xcenter - diameter/2
    Y = Ycenter - diameter/2
    deltaX = diameter/20
    text_height = number_height * diameter
    textSize(text_height)
    delta = text_height/2
    # 3:1 scale
    push()
    translate(X, Y)
    clipRectangle(-diameter/3, -0.1*diameter, deltaX, 1.1*diameter)
    translate(-deltaX/1.5, 0)
    line(0, 0, 0, diameter)
    n = 60
    for ix in range(n+1):
        y = ix/n * diameter
        if ix % 3 == 0:
            line(0, y, -delta*2, y)
            rmove(-delta/2, -text_height/2.5)
            rtext("%d" % ix)
        else:
            line(0, y, -delta, y)
    rmove(2*delta, 3*delta)
    textSize(1.3*text_height)
    rtext("3:1")
    textSize(text_height)
    # 2:1 scale
    translate(-deltaX, 0)
    line(0, 0, 0, diameter)
    n = 40
    for ix in range(n+1):
        y = ix/n * diameter
        if ix % 2 == 0:
            line(0, y, -delta*2, y)
            rmove(-delta/2, -text_height/3)
            rtext("%d" % ix)
        else:
            line(0, y, -delta, y)
    rmove(2*delta, 3*delta)
    textSize(1.3*text_height)
    rtext("2:1")
    textSize(text_height)
    pop()
    # 4:1 scale
    X = Xcenter + diameter/2
    Y = Ycenter - diameter/2
    translate(X, Y)
#   clipRectangle(0, -0.1*diameter, 2.5*deltaX, 1.1*diameter)
    translate(deltaX/1.5, 0)
    line(0, 0, 0, diameter)
    n = 80
    for ix in range(n+1):
        y = ix/n * diameter
        if ix % 4 == 0:
            line(0, y, delta*2, y)
            rmove(delta/2, -text_height/2.5)
            text("%d" % ix)
        else:
            line(0, y, delta, y)
    rmove(2*delta, 3*delta)
    textSize(1.3*text_height)
    rtext("4:1")
    textSize(text_height)
    # 5:1 scale
    translate(deltaX, 0)
    line(0, 0, 0, diameter)
    n = 100
    for ix in range(n+1):
        y = ix/n * diameter
        if ix % 5 == 0:
            line(0, y, delta*2, y)
            rmove(delta/2, -text_height/2.5)
            text("%d" % ix)
        else:
            line(0, y, delta, y)
    rmove(2*delta, 3*delta)
    textSize(1.3*text_height)
    rtext("5:1")
    textSize(text_height)
    pop()

def main():
    os = open(vd("out/maneuv_board.ps"), "w")
    ginitialize(os)
    setOrientation(landscape, inches)
    translate(-.68,-0.01)  # Appears to get origin to lower left corner
    lineColor(base_line_color)
    lineWidth(tick_line_width)
    textColor(base_line_color)
    DrawBoard(Diameter)
    os.close()

def DrawBoard(diameter):
    push()
    DrawDegreeScale(diameter)
    if draw_scales:
        DrawScales(diameter)
    if draw_nomograph:
        DrawNomograph(diameter)
    pop()

def UpperLogScale(length, fmt, font_scale, start=1, last=1):
    '''Draw a log scale from P0 = (0, 0) to P1 = (1, 0).  Label
    each point using the fmt string.  The 1 value starts at P0 and
    10 is at P1.  Numbers are printed on each integer using the format 
    in fmt.  start represents the value at the beginning of the scale.
    For example, start = 1000 means the scale goes from 1000 to 10000.
    If last is true, label the last number.
 
    We assume the origin is the beginning of the scale.  The calling
    context should set the scale so that 1 is the end of the scale.
    The scale extends along the positive X axis.
    '''
    push()
    assert(start > 0)
    text_height = number_height * font_scale * 1.2
    textSize(text_height)
    major_tick = text_height
    med_tick   = major_tick*2/3
    minor_tick = major_tick/3
    line(0, 0, length, 0)
    # Draw minor ticks
    for ix in range(10, 60):
        x = log10(ix/10) * length
        line(x, 0, x, minor_tick)
    # Draw medium ticks
    for ix in range(15, 100, 5):
        x = log10(ix/10) * length
        line(x, 0, x, med_tick)
    # Draw major ticks and label them
    for ix in range(1, 11):
        x = abs(log10(ix/10) + 1) * length
        line(x, 0, x, major_tick)
        push()
        translate(x, 0)
        rotate(90)
        move(1.2*major_tick, -text_height/3)
        if ix == 10 and not last:
            pop()
            continue
        text(fmt % (ix*start))
        pop()
    pop()

def LowerLogScale(length, fmt, font_scale, start=1, last=1):
    '''Same as UpperLogScale, but ticks and text are below the horizontal
    line.
    '''
    push()
    assert(start > 0)
    text_height = number_height * font_scale * 1.2
    textSize(text_height)
    major_tick = text_height
    med_tick   = major_tick*2/3
    minor_tick = major_tick/3
    line(0, 0, length, 0)
    # Draw minor ticks
    for ix in range(10, 60):
        x = log10(ix/10) * length
        line(x, 0, x, minor_tick)
    # Draw medium ticks
    for ix in range(15, 100, 5):
        x = log10(ix/10) * length
        line(x, 0, x, med_tick)
    # Draw major ticks and label them
    for ix in range(1, 11):
        x = abs(log10(ix/10) + 1) * length
        line(x, 0, x, major_tick)
        push()
        translate(x, 0)
        rotate(90)
        move(1.2*major_tick, -text_height/3)
        if ix == 10 and not last:
            pop()
            continue
        text(fmt % (ix*start))
        pop()
    pop()

def DrawLongLogScale(X, Y, diameter, length, label):
    push()
    text_height = number_height * diameter
    textSize(text_height)
    translate(X, Y)
    UpperLogScale(length, "%.1f", font_scale=diameter, start=0.1, last=0)
    translate(length, 0)
    UpperLogScale(length, "%.0f", font_scale=diameter, start=1, last=0)
    translate(length, 0)
    UpperLogScale(length, "%.0f", font_scale=diameter, start=10, last=0)
    translate(length, 0)
    UpperLogScale(length, "%.0f", font_scale=diameter, start=100, last=1)
    move(1.05*length, 2*text_height)
    text(label)
    pop()

def DrawNomograph(diameter):
    push()
    deltaX = diameter/10
    deltaY = diameter/15
    X = Xcenter - diameter/2 - deltaX
    Y = Ycenter - diameter/2 - deltaY
    text_height = number_height * diameter
    textSize(text_height)
    # Draw the main log scale for time in minutes
    length = Diameter/2.3
    DrawLongLogScale(X, Y, diameter, length, "minutes")
    # Draw the speed log scale in mph and km/hr
    s = 1.3
    DrawLongLogScale(X, Y - 2*s*deltaY, diameter, length, "mph")
    pop()

main()

